﻿using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

namespace RulesEngine.Configuration
{
    public class RulesEngineConfigurationXmlProvider<TCandidate>
    {
        private const string CM_NameSpace = "urn:schemas-ldh-com:config-manager-1";

        private XDocument _xDoc;

        public RulesEngineConfigurationXmlProvider()
        {
        }

        #region Load and Validate

        public void Load(XmlDocument xmlDocument)
        {
            Validate(xmlDocument.OuterXml);
            XmlNodeReader xnr = new XmlNodeReader(xmlDocument);
            _xDoc = XDocument.Load(xnr);
        }

        public void LoadXml(string xml)
        {
            Validate(xml);
            _xDoc = XDocument.Parse(xml);
        }

        private XmlSchema GetXmlSchema()
        {
            string xsdFilePath = @"D:\\RuleConfig\\RulesEngineSchema.xsd";
            XmlSchemaSet schemaSet = new XmlSchemaSet();
            schemaSet.Add(null, xsdFilePath);
            XmlSchema schema = null;

            foreach (XmlSchema xs in schemaSet.Schemas())
            {
                schema = xs;
            }
            return schema;
        }

        protected virtual void Validate(string xml)
        {
            StringReader stringReader = null;
            try
            {
                stringReader = new StringReader(xml);
                XmlTextReader xmlReader = new XmlTextReader(stringReader);
                string engineXml = string.Empty;

                while (xmlReader.Read())
                {
                    if (xmlReader.NodeType == XmlNodeType.Element && xmlReader.LocalName == "RulesEngine")
                    {
                        engineXml = xmlReader.ReadOuterXml();
                        break;
                    }
                }

                if (string.IsNullOrEmpty(engineXml))
                {
                    throw new RuleConfigurationException("Xml does not contain a node named 'RulesEngine'");
                }

                stringReader.Close();
                stringReader.Dispose();
                stringReader = new StringReader(engineXml);
                xmlReader = new XmlTextReader(stringReader);

                XmlSchema xmlSchema = GetXmlSchema();

                var validatingSettings = new XmlReaderSettings { ValidationType = ValidationType.Schema };
                validatingSettings.Schemas.Add(xmlSchema);
                var xValidator = XmlReader.Create(xmlReader, validatingSettings);
                while (xValidator.Read())
                {
                }
                xValidator.Close();
            }
            catch (XmlSchemaException err)
            {
                string message = "RulesEngine configuration failed Xml Schema Validation.";
                throw new RuleConfigurationException(message, err);
            }
            finally
            {
                if (stringReader != null)
                    stringReader.Dispose();
            }
        }

        #endregion Load and Validate

        #region Rules and Rule Clauses

        protected IRule<TCandidate> GetRuleFromConfig(XElement ruleElement, RuleSetConfigInitData configInitData)
        {
            string ruleAlias = ruleElement.Attribute("Name")!.Value.Trim();

            if (!configInitData.RuleTypes.ContainsKey(ruleAlias))
                throw new RuleConfigurationException(string.Format("Type does not exist in dictionary for Rule [{0}]", ruleAlias));
            Type type = configInitData.RuleTypes[ruleAlias];

            IRule<TCandidate>? rule = Activator.CreateInstance(type) as IRule<TCandidate>;
            if (rule != null)
            {
                IEnumerable<XAttribute> attrList =
                    from attribute in ruleElement.Attributes()
                    where attribute.Name != "Name" && string.Compare(attribute.Name.NamespaceName, CM_NameSpace, true) != 0 //discard Config Manager attributes
                    select attribute;

                foreach (XAttribute attribute in attrList)
                {
                    string propName = attribute.Name.LocalName.Trim();
                    string propValue = attribute.Value.Trim();

                    RuleUtility.SetPropertyValue(rule, propName, propValue);
                }
            }

            return rule!;
        }

        protected IRule<TCandidate> GetRuleClauses(XElement ruleClauseElement, RuleSetConfigInitData configInitData)
        {
            if (ruleClauseElement.Name.LocalName == "Rule")
            {
                return GetRuleFromConfig(ruleClauseElement, configInitData);
            }

            if (ruleClauseElement.Name.LocalName == "ALL")
            {
                return GetAllRuleClauseFromConfig(ruleClauseElement, configInitData);
            }

            if (ruleClauseElement.Name.LocalName == "NOT")
            {
                return GetNotRuleClauseFromConfig(ruleClauseElement, configInitData);
            }

            if (ruleClauseElement.Name.LocalName == "ANY")
            {
                return GetAnyRuleClauseFromConfig(ruleClauseElement, configInitData);
            }

            if (ruleClauseElement.Name.LocalName == "NONE")
            {
                return GetNoneRuleClauseFromConfig(ruleClauseElement, configInitData);
            }

            if (ruleClauseElement.Name.LocalName == "NOT-ALL")
            {
                return GetNotAllRuleClauseFromConfig(ruleClauseElement, configInitData);
            }

            throw new RuleConfigurationException(string.Format("Unrecognized Rule Clause Element: [{0}]", ruleClauseElement.Name.LocalName));
        }

        protected IRule<TCandidate> GetAllRuleClauseFromConfig(XElement ruleElement, RuleSetConfigInitData configInitData)
        {
            IEnumerable<XElement> childRuleList =
                from el in ruleElement.Elements()
                select el;

            IRule<TCandidate> rule = null;
            IRule<TCandidate> otherRule = null;

            foreach (XElement clause in childRuleList)
            {
                if (rule == null)
                {
                    rule = GetRuleClauses(clause, configInitData);
                }
                else
                {
                    otherRule = GetRuleClauses(clause, configInitData);
                    rule = rule.And(otherRule);
                }
            }

            return rule;
        }

        protected IRule<TCandidate> GetAnyRuleClauseFromConfig(XElement ruleElement, RuleSetConfigInitData configInitData)
        {
            IEnumerable<XElement> childRuleList =
                from el in ruleElement.Elements()
                select el;

            IRule<TCandidate> rule = null;
            IRule<TCandidate> otherRule = null;

            foreach (XElement clause in childRuleList)
            {
                if (rule == null)
                {
                    rule = GetRuleClauses(clause, configInitData);
                }
                else
                {
                    otherRule = GetRuleClauses(clause, configInitData);
                    rule = rule.Or(otherRule);
                }
            }

            return rule;
        }

        protected IRule<TCandidate> GetNotRuleClauseFromConfig(XElement ruleElement, RuleSetConfigInitData configInitData)
        {
            IEnumerable<XElement> childRuleList =
                from el in ruleElement.Elements()
                select el;

            IRule<TCandidate> rule = null;
            rule = GetRuleClauses(childRuleList.First(), configInitData);
            rule = rule.Not();

            return rule;
        }

        protected IRule<TCandidate> GetNoneRuleClauseFromConfig(XElement ruleElement, RuleSetConfigInitData configInitData)
        {
            IEnumerable<XElement> childRuleList =
                from el in ruleElement.Elements()
                select el;

            IRule<TCandidate> rule = null;
            IRule<TCandidate> otherRule = null;

            foreach (XElement clause in childRuleList)
            {
                if (rule == null)
                {
                    rule = GetRuleClauses(clause, configInitData);
                }
                else
                {
                    otherRule = GetRuleClauses(clause, configInitData);
                    rule = rule.Or(otherRule);
                }
            }

            if (rule != null)
                rule = rule.Not();

            return rule;
        }

        protected IRule<TCandidate> GetNotAllRuleClauseFromConfig(XElement ruleElement, RuleSetConfigInitData configInitData)
        {
            IEnumerable<XElement> childRuleList =
                from el in ruleElement.Elements()
                select el;

            IRule<TCandidate> rule = null;
            IRule<TCandidate> otherRule = null;

            foreach (XElement clause in childRuleList)
            {
                if (rule == null)
                {
                    rule = GetRuleClauses(clause, configInitData);
                }
                else
                {
                    otherRule = GetRuleClauses(clause, configInitData);
                    rule = rule.And(otherRule);
                }
            }

            if (rule != null)
                rule = rule.Not();

            return rule;
        }

        #endregion Rules and Rule Clauses

        #region Rule Groups

        protected RuleGroup<TCandidate> GetRuleGroupFromConfig(XElement ruleGroupElement, RuleSetConfigInitData configInitData)
        {
            RuleGroup<TCandidate> ruleGroup = new RuleGroup<TCandidate>();
            var xAttrName = ruleGroupElement.Attribute("Name");
            if (xAttrName != null)
                ruleGroup.Name = xAttrName.Value.Trim();
            var xAttrDesc = ruleGroupElement.Attribute("Description");
            if (xAttrDesc != null)
                ruleGroup.Description = xAttrDesc.Value.Trim();

            IEnumerable<XElement> ruleList =
                from ruleElement in ruleGroupElement.Elements("Rules").Elements()
                select ruleElement;

            foreach (XElement ruleElement in ruleList)
            {
                IRule<TCandidate> rule = GetRuleClauses(ruleElement, configInitData);
                ruleGroup.Rules.Add(rule);
            }

            XElement ruleGroupResultElement =
                (
                    from groupResultElement in ruleGroupElement.Elements("GroupResult")
                    select groupResultElement
                ).SingleOrDefault()!;

            if (ruleGroupResultElement != null)
            {
                var processor = GetRuleGroupResultProcessorFromConfig(ruleGroupResultElement, configInitData);
                ruleGroup.ResultProcessor = processor;
            }

            return ruleGroup;
        }

        protected IGroupResultProcessor<TCandidate> GetRuleGroupResultProcessorFromConfig(XElement ruleGroupResultElement, RuleSetConfigInitData configInitData)
        {
            string processorAlias = null;
            var xAttrName = ruleGroupResultElement.Attribute("Name");
            if (xAttrName != null)
            {
                processorAlias = xAttrName.Value.Trim();
            }
            if (string.IsNullOrWhiteSpace(processorAlias))
                throw new RuleConfigurationException(string.Format("'Name' attribute of 'GroupResult' element is null or empty", processorAlias));
            if (!configInitData.RuleGroupResultProcessorTypes.ContainsKey(processorAlias))
                throw new RuleConfigurationException(string.Format("Type does not exist in dictionary for Rule Group Result Processor [{0}]", processorAlias));
            Type type = configInitData.RuleGroupResultProcessorTypes[processorAlias];

            var processor = Activator.CreateInstance(type) as IGroupResultProcessor<TCandidate>;
            if (processor != null)
            {
                IEnumerable<XAttribute> attrList =
                    from attribute in ruleGroupResultElement.Attributes()
                    where attribute.Name != "Name"
                       && string.Compare(attribute.Name.NamespaceName, CM_NameSpace, true) != 0 //discard Config Manager attributes
                    select attribute;

                foreach (XAttribute attribute in attrList)
                {
                    string propName = attribute.Name.LocalName.Trim();
                    string propValue = attribute.Value.Trim();

                    RuleUtility.SetPropertyValue(processor, propName, propValue);
                }
            }

            return processor;
        }

        #endregion Rule Groups

        #region Rule Sets

        protected RuleSet<TCandidate> GetRuleSetFromConfig(XElement ruleSetElement, RuleSetConfigInitData configInitData)
        {
            RuleSet<TCandidate> ruleSet = new RuleSet<TCandidate>();
            ruleSet.Name = ruleSetElement.Attribute("Name").Value.Trim();
            ruleSet.StartDate = DateTime.Parse(ruleSetElement.Attribute("StartDate").Value.Trim());
            ruleSet.EndDate = DateTime.Parse(ruleSetElement.Attribute("EndDate").Value.Trim());

            IEnumerable<XElement> groupList =
                from groupElement in ruleSetElement.Elements("Group")
                select groupElement;

            foreach (XElement groupElement in groupList)
            {
                RuleGroup<TCandidate> group = GetRuleGroupFromConfig(groupElement, configInitData);
                ruleSet.Groups.Add(group);
            }

            return ruleSet;
        }

        #endregion Rule Sets

        #region Rule Types

        protected RuleTypeDictionary GetRuleTypesFromConfig(XElement ruleTypesElement)
        {
            RuleTypeDictionary typeDictionary = new RuleTypeDictionary();

            try
            {
                IEnumerable<XElement> typeList =
                    from typeElement in ruleTypesElement.Elements("add")
                    select typeElement;

                string key, typeName;

                foreach (XElement addTypeElement in typeList)
                {
                    key = addTypeElement.Attribute("Key").Value.Trim();
                    typeName = addTypeElement.Attribute("Type").Value.Trim();

                    Type type = Type.GetType(typeName);
                    if (type == null)
                        throw new RuleConfigurationException(string.Format("Could not get type for configured RuleType [{0}]", typeName));
                    if (!(Activator.CreateInstance(type) is IRule<TCandidate>))
                        throw new RuleConfigurationException(string.Format("Could not get type for configured RuleType [{0}].  Type does not implement IRule<{1}>", typeName, typeof(TCandidate).ToString()));

                    typeDictionary.Add(key, type);
                }
            }
            catch (RuleConfigurationException rcex)
            {
                throw rcex;
            }
            catch (Exception ex)
            {
                string msg = string.Format("Could not get RuleTypeDictionary object based on configured values.");
                throw new RuleConfigurationException(msg, ex);
            }

            return typeDictionary;
        }

        protected XElement GetRuleTypesElement(XElement engineElement)
        {
            IEnumerable<XElement> ruleTypesList =
                from el in engineElement.Elements("RuleTypes")
                select el;

            XElement ruleTypesElement = ruleTypesList.First();

            return ruleTypesElement;
        }

        #endregion Rule Types

        #region Group Result Types

        protected RulesEngineTypeDictionary GetRuleGroupResultProcessorTypesFromConfig(XElement processorTypesElement)
        {
            var typeDictionary = new RulesEngineTypeDictionary();

            try
            {
                IEnumerable<XElement> typeList =
                    from typeElement in processorTypesElement.Elements("add")
                    select typeElement;

                string key, typeName;

                foreach (XElement addTypeElement in typeList)
                {
                    key = addTypeElement.Attribute("Key").Value.Trim();
                    typeName = addTypeElement.Attribute("Type").Value.Trim();

                    Type type = Type.GetType(typeName);
                    if (type == null)
                        throw new RuleConfigurationException(string.Format("Could not get type for configured ResultType [{0}]", typeName));
                    if (!(Activator.CreateInstance(type) is IGroupResultProcessor<TCandidate>))
                        throw new RuleConfigurationException(string.Format("Could not get type for configured ResultType [{0}].  Type does not implement IGroupResultProcessor<{1}>", typeName, typeof(TCandidate).ToString()));

                    typeDictionary.Add(key, type);
                }
            }
            catch (RuleConfigurationException rcex)
            {
                throw rcex;
            }
            catch (Exception ex)
            {
                string msg = string.Format("Could not get RuleTypeDictionary object based on configured values.");
                throw new RuleConfigurationException(msg, ex);
            }

            return typeDictionary;
        }

        protected XElement GetRuleGroupResultTypesElement(XElement engineElement)
        {
            IEnumerable<XElement> typesList =
                from el in engineElement.Elements("RuleGroupResultProcessorTypes")
                select el;

            XElement typesElement = typesList.FirstOrDefault();

            return typesElement;
        }

        #endregion Group Result Types

        #region Rules Engine

        /// <summary>
        /// Get Rules Engine From xml configuration
        /// </summary>
        /// <typeparam name="TRulesEngine">Custom type of RulesEngine. Should inherit from RulesEngine</typeparam>
        /// <param name="rulesEngine">Object of type TRulesEngine which may already be initialized with some values.</param>
        /// <returns>The input rulesEngine object with values set based on configuration.</returns>
        public virtual TRulesEngine GetRulesEngine<TRulesEngine>(TRulesEngine rulesEngine) where TRulesEngine : RulesEngine<TCandidate>
        {
            if (rulesEngine == null)
                throw new ArgumentException("Argument cannot be null.", "rulesEngine");

            var engine = rulesEngine;
            RuleSetConfigInitData configInitData = new RuleSetConfigInitData();

            XElement engineElement = GetRulesEngineElement();

            XElement ruleTypesElement = GetRuleTypesElement(engineElement);
            RuleTypeDictionary ruleTypes = GetRuleTypesFromConfig(ruleTypesElement);
            engine.RuleTypes = ruleTypes;
            configInitData.RuleTypes = engine.RuleTypes;

            XElement ruleGrpRsltTypesElement = GetRuleGroupResultTypesElement(engineElement);
            if (ruleGrpRsltTypesElement != null)
            {
                RulesEngineTypeDictionary grpResultTypes = GetRuleGroupResultProcessorTypesFromConfig(ruleGrpRsltTypesElement);
                engine.RuleGroupResultProcessorTypes = grpResultTypes;
                configInitData.RuleGroupResultProcessorTypes = engine.RuleGroupResultProcessorTypes;
            }

            IEnumerable<XElement> ruleSetList =
                from el in engineElement.Elements("RuleSet")
                select el;

            foreach (XElement ruleSetElement in ruleSetList)
            {
                RuleSet<TCandidate> ruleSet = GetRuleSetFromConfig(ruleSetElement, configInitData);
                engine.RuleSets.Add(ruleSet);
            }

            return engine;
        }

        /// <summary>
        /// Get Rules Engine From xml configuration
        /// </summary>
        /// <typeparam name="TRulesEngine">Custom type of RulesEngine. Should inherit from RulesEngine<TCandidate>.</typeparam>
        /// <returns>Object of type TRulesEngine</returns>
        public virtual TRulesEngine GetRulesEngine<TRulesEngine>() where TRulesEngine : RulesEngine<TCandidate>, new()
        {
            var engine = new TRulesEngine();
            GetRulesEngine<RulesEngine<TCandidate>>(engine);
            return engine;
        }

        /// <summary>
        /// Get Rules Engine From xml configuration
        /// </summary>
        /// <returns>Object of type RulesEngine<TCandidate></returns>
        public virtual RulesEngine<TCandidate> GetRulesEngine()
        {
            var engine = new RulesEngine<TCandidate>();
            GetRulesEngine(engine);
            return engine;
        }

        protected XElement GetRulesEngineElement()
        {
            IEnumerable<XElement> engineList =
                from el in _xDoc.Descendants("RulesEngine")
                select el;

            if (!engineList.Any())
                throw new RuleConfigurationException("Xml Document does not contain a RulesEngine Element");
            if (engineList.Count() > 1)
                throw new RuleConfigurationException("Xml Document must not contain more than one RulesEngine Element");

            XElement engineElement = engineList.First();

            return engineElement;
        }

        #endregion Rules Engine
    }
}